﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.IO;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Microsoft.Office.OneNote.Interop;
using System.Collections;

namespace Microsoft.Office.OneNote
{
    /// <summary>
    /// Represents the root of the OneNote hierarchy.
    /// </summary>
    [OneNoteXmlElementAttribute("Notebooks")]
    public sealed partial class OneNoteHierarchy : IOneNoteHierarchy
    {
        private bool _dirty;

        private readonly OneNoteHierarchyObjectStorage _storage;
        private readonly OneNoteObjectCollection<OneNoteNotebook> _notebooks;

        private OneNoteOpenSections _openSections;
        private OneNoteUnfiledNotes _unfiledNotes;

        /// <summary>
        /// Initializes a new instance of the <see cref="OneNoteHierarchy"/> class.
        /// </summary>
        /// <param name="application">The application.</param>
        private OneNoteHierarchy()
        {
            _storage = new OneNoteHierarchyObjectStorage(this);
            _notebooks = new OneNoteObjectCollection<OneNoteNotebook>(_storage);

            // this event handler must be attached AFTER the _notebooks collection
            // is constructed, in order to be called AFTER the _notebooks collection's
            // handler. 

            // the OneNoteUnfiledNotes..ctor(OneNoteHierarchy) requires a fully initialized
            // and loaded Notebooks collections to find the section.

            _storage.Updated += OnStorageUpdated;

            Update();
        }

        public OneNoteObjectCollection<OneNoteNotebook> Notebooks
        {
            get { return _notebooks; }
        }

        public OneNoteObjectCollection<OneNoteSection> OpenSections
        {
            get { return _openSections.Sections; }
        }

        public OneNotePage CurrentlyViewedPage
        {
            get
            {
                if (UnfiledNotes.IsCurrentlyViewed)
                {
                    return UnfiledNotes.Section.Pages.SingleOrDefault(p => p.IsCurrentlyViewed);
                }

                if (_openSections.IsCurrentlyViewed)
                {
                    return FindCurrentlyViewedPage(_openSections.Sections);
                }

                return (from n in Notebooks
                        where n.IsCurrentlyViewed
                        select FindCurrentlyViewedPage(n.Sections)).SingleOrDefault();
            }
        }

        [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Unfiled")]
        public OneNoteUnfiledNotes UnfiledNotes
        {
            get { return _unfiledNotes; }
        }

        public OneNoteSection OpenSection(string path)
        {
            OneNoteNotebook ancestor = GetAncestorNotebookOf(path);

            if (ancestor != null)
            {
                return ancestor.GetSectionByPath(path);
            }

            return _openSections.Open(path);
        }

        /// <summary>
        /// Opens a notebook at the specified location
        /// or creates a new one.
        /// </summary>
        /// <param name="path">The location of the notebook.</param>
        /// <param name="openExisting">
        /// If set to <c>true</c>, opens only an existing notebook.
        /// Otherwise, may create a new one.</param>
        /// <returns></returns>
        /// <remarks>
        /// <para>
        /// If the location is a section group inside
        /// an open notebook, that notebook is returned.
        /// </para>
        /// <para>
        /// If the directory tree specified by the path contains any
        /// open notebooks, they will be closed and become available as
        /// section groups inside the returned notebook. The old notebook
        /// objects become invalid and all references to them must be
        /// discarded.
        /// </para>
        /// </remarks>
        public OneNoteNotebook OpenNotebook(string path, bool openExisting)
        {
            Contract.RequiresNotNullNorEmpty(path, "path");

            if (!Path.IsPathRooted(path))
            {
                path = Path.Combine(
                    OneNoteApplication.Current.GetSpecialLocation(SpecialLocation.DefaultNotebookFolder), 
                    path);
            }

            OneNoteNotebook ancestor = GetAncestorNotebookOf(path);

            if (ancestor != null)
            {
                // path is already open as a section in the ancestor
                return ancestor;
            }

            return _storage.Open<OneNoteNotebook>(path, OneNoteObjectId.Empty, openExisting);
        }

        /// <summary>
        /// Opens a notebook at the specified location, 
        /// or creates a new one.
        /// </summary>
        /// <param name="path">The location of the notebook.</param>
        /// <returns></returns>
        /// <remarks>
        /// <para>
        /// If the path location is a section group inside
        /// an open notebook, that notebook is returned.
        /// </para>
        /// <para>
        /// If the directory tree specified by the path contains any
        /// open notebooks, they will be closed and become available as
        /// section groups inside the returned notebook. The old notebook
        /// objects become invalid and all references to them must be
        /// discarded.
        /// </para>
        /// </remarks>
        public OneNoteNotebook OpenNotebook(string path)
        {
            return OpenNotebook(path, false);
        }

        /// <summary>
        /// Closes the specified notebook.
        /// </summary>
        /// <param name="notebook">The notebook to be closed.</param>
        [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
        public void CloseNotebook(OneNoteNotebook notebook)
        {
            Contract.RequiresNotNull(notebook, "notebook");

            OneNoteApplication.Current.CloseNotebook(notebook.ObjectId);
            Update();
        }

        public void Update()
        {
            // we need to have access to all sections at any time, 
            // because we might need to find an UnfiledNotes section
            // inside the open notebooks.

            XDocument document = GetHierarchy(HierarchyScope.Sections);
            _storage.UpdateFrom(document.Root.Elements());
        }

        public void UpdateRecursive()
        {
            XDocument document = GetHierarchy(HierarchyScope.Pages);
            _storage.UpdateFrom(document.Root.Elements());
        }

        public void SubmitChanges()
        {
            if (_dirty)
            {
                XElement element = OneNoteSchema.CreateElement(this);
                _storage.WriteTo(element);

                XDocument changes = OneNoteSchema.CreateDocument(element);
                OneNoteApplication.Current.UpdateHierarchy(changes);
            }

            _dirty = false;
        }

        public bool HasConflicts
        {
            get { return _storage.HasConflicts; }
        }

        private OneNoteNotebook GetAncestorNotebookOf(string path)
        {
            return Notebooks.Where(n => n.IsAncestorOf(path)).SingleOrDefault();
        }

        private void OnStorageUpdated(object sender, EventArgs e)
        {
            _openSections = _storage.OfType<OneNoteOpenSections>().SingleOrDefault()
                ?? new OneNoteOpenSections(this);

            // when there is no <one:UnfiledNotes> element, thus ther are no 
            // OneNoteUnfiledNotes objects in the _storage, we have to
            // create a special one, which finds the section by its path in
            // the open notebooks.
            // it is vital for this handler to be executed after the 
            // Notebooks collection _storage.Updated handler.

            var newUnfiledNotes = _storage.OfType<OneNoteUnfiledNotes>().SingleOrDefault();

            if (newUnfiledNotes != null)
            {
                if (UnfiledNotes == newUnfiledNotes)
                {
                    return;
                }

                if (UnfiledNotes != null)
                {
                    UnfiledNotes.MarkInvalid();
                }

                _unfiledNotes = newUnfiledNotes;
            }
            else
            {
                if (UnfiledNotes != null && UnfiledNotes.SectionIsOpenElsewhere)
                {
                    UnfiledNotes.ReloadSection();
                }
                else
                {
                    // the section was invalidated by _storage, so we have
                    // to create a new one.

                    _unfiledNotes = new OneNoteUnfiledNotes(this);
                }
            }
        }

        OneNoteHierarchy IOneNoteHierarchy.Hierarchy
        {
            get { return this; }
        }
        
        void IOneNoteHierarchy.MarkChildrenDirty()
        {
            _dirty = true;
        }

        private static XDocument GetHierarchy(HierarchyScope scope)
        {
            return OneNoteApplication.Current.GetHierarchy(OneNoteObjectId.Empty, scope);
        }

        private static OneNotePage FindCurrentlyViewedPage(IEnumerable<OneNotePage> pages)
        {
            return pages.SingleOrDefault(p => p.IsCurrentlyViewed);
        }

        private static OneNotePage FindCurrentlyViewedPage(IEnumerable<OneNoteSection> sections)
        {
            return FindCurrentlyViewedPage(sections.SingleOrDefault(s => s.IsCurrentlyViewed));
        }

        private static OneNotePage FindCurrentlyViewedPage(IEnumerable<OneNoteSectionBase> sections)
        {
            return FindCurrentlyViewedPage(sections.SingleOrDefault(s => s.IsCurrentlyViewed));
        }

        private static OneNotePage FindCurrentlyViewedPage(OneNoteSectionBase root)
        {
            if (root != null)
            {
                return FindCurrentlyViewedPage(root.Pages) ??
                       FindCurrentlyViewedPage(root.Sections);
            }

            return null;
        }
    }
}
